package org.jpropelleralt.jview.ref.impl;

import java.util.Map;

import javax.swing.BoundedRangeModel;
import javax.swing.DefaultBoundedRangeModel;
import javax.swing.JComponent;
import javax.swing.JProgressBar;
import javax.swing.JSlider;

import org.jpropelleralt.box.Box;
import org.jpropelleralt.box.Boxes;
import org.jpropelleralt.change.Change;
import org.jpropelleralt.jview.JView;
import org.jpropelleralt.ref.Ref;
import org.jpropelleralt.view.View;
import org.jpropelleralt.view.Views;
import org.jpropelleralt.view.update.UpdateManager;

/**
 * An editing {@link JView} for a {@link Ref} with value of type {@link Integer}
 */
public class NumberRangeView implements JView {

	/**
	 * Types of control
	 */
	public enum ControlType {
		/**
		 * Display as {@link JSlider}
		 */
		SLIDER,
		
		/**
		 * Display as {@link JProgressBar}
		 */
		PROGRESS_BAR
	}

	private final Ref<Integer> value;
	private final Ref<Boolean> locked;
	
	private final UpdateManager updateManager;

	private final JComponent component;

	private final AutoBoundedRangeModel model;
	
	private NumberRangeView(final Ref<Integer> value, int min, int max, Ref<Boolean> locked, UpdateManager updateManager, ControlType controlType) {
		super();
		this.value = value;
		this.locked = locked;

		model = new AutoBoundedRangeModel(min, max);
		if (controlType == ControlType.SLIDER) {
			this.component = new LinkingJSlider(this, model);			
		} else {
			this.component = new LinkingJProgressBar(this, model);
		}
		
		this.updateManager = updateManager;
		updateManager.registerUpdatable(this);

		value.features().addView(this);
		if (locked != null) {
			locked.features().addView(this);
		}
		
		//Initial update
		update();
	}
	
	@Override
	public void update() {
		//Get our model to tell the slider value has changed
		model.fire();
		
		//Fix enabled state
		boolean shouldEnable = !Boxes.isTrue(locked);
		if (component.isEnabled() != shouldEnable) {
			component.setEnabled(shouldEnable);
		}
	}

	private class AutoBoundedRangeModel extends DefaultBoundedRangeModel {
		
		private int lastValue = 0;
		
		public AutoBoundedRangeModel(int min, int max) {
			super(min, 0, min, max);
		}
		
		public void fire() {
			Integer refValue = value.get();
			
			//If necessary, extend range to cover value we have seen
			if (refValue != null) {
				if (refValue < getMinimum()) {
					setMinimum(refValue);
				}
				if (refValue > getMaximum()) {
					setMaximum(refValue);
				}
			}

			fireStateChanged();
		}
		
		@Override
		public int getValue() {
			return value.get();
		}
		
		@Override
		public int getExtent() {
			return 0;
		}
		
		@Override
		public void setValue(int n) {
			lastValue = n;
		}
		
		@Override
		public void setValueIsAdjusting(boolean b) {
			super.setValueIsAdjusting(b);
			
			//When we stop adjusting, use the last set value
			if (!b) {
				Integer refValue = value.get();
				if (refValue == null || !refValue.equals(lastValue)) {
					value.set(lastValue);					
				}
			}
		}
		
	}

	@Override
	public void changes(Map<Box, Change> changes) {
		updateManager.updateRequiredBy(this);
	}

	/**
	 * Get the {@link JComponent} used for display/editing
	 * 
	 * @return		The {@link JComponent}
	 */
	public JComponent getComponent() {
		return component;
	}
	
	@Override
	public boolean selfNaming() {
		//Just displays value
		return false;
	}
	
	@Override
	public Format format() {
		return Format.SINGLE_LINE;
	}
	
	/**
	 * Create a {@link NumberRangeView}
	 * @param value				The value to display, as a {@link Ref}
	 * @param min				The minimum value in range
	 * @param max				The maximum value in range
	 * @param locked			While this is true, view will not permit editing (control disabled) 
	 * @param updateManager		The {@link UpdateManager} to use
	 * @param controlType		The type of control to use for display/editing
	 * @return					a new {@link NumberRangeView}
	 */
	public static NumberRangeView create(final Ref<Integer> value, int min, int max, Ref<Boolean> locked, UpdateManager updateManager, ControlType controlType) {
		return new NumberRangeView(value, min, max, locked, updateManager, controlType);
	}

	/**
	 * Create a {@link NumberRangeView}
	 * @param value				The value to display, as a {@link Ref}
	 * @param min				The minimum value in range
	 * @param max				The maximum value in range
	 * @param locked			While this is true, view will not permit editing (control disabled) 
	 * @param controlType		The type of control to use for display/editing
	 * @return					a new {@link NumberRangeView}
	 */
	public static NumberRangeView create(final Ref<Integer> value, int min, int max, Ref<Boolean> locked, ControlType controlType) {
		return new NumberRangeView(value, min, max, locked, Views.updateManager(), controlType);
	}
	
	/**
	 * Create a {@link NumberRangeView}
	 * @param value				The value to display, as a {@link Ref}
	 * @param min				The minimum value in range
	 * @param max				The maximum value in range
	 * @param controlType		The type of control to use for display/editing
	 * @return					a new {@link NumberRangeView}
	 */
	public static NumberRangeView create(final Ref<Integer> value, int min, int max, ControlType controlType) {
		return new NumberRangeView(value, min, max, null, Views.updateManager(), controlType);
	}

	/**
	 * Create a {@link NumberRangeView} as a slider
	 * @param value				The value to display, as a {@link Ref}
	 * @param min				The minimum value in range
	 * @param max				The maximum value in range
	 * @return					a new {@link NumberRangeView}
	 */
	public static NumberRangeView createSlider(final Ref<Integer> value, int min, int max) {
		return new NumberRangeView(value, min, max, null, Views.updateManager(), ControlType.SLIDER);
	}

	/**
	 * Create a {@link NumberRangeView} as a progress bar
	 * @param value				The value to display, as a {@link Ref}
	 * @param min				The minimum value in range
	 * @param max				The maximum value in range
	 * @return					a new {@link NumberRangeView}
	 */
	public static NumberRangeView createProgressBar(final Ref<Integer> value, int min, int max) {
		return new NumberRangeView(value, min, max, null, Views.updateManager(), ControlType.PROGRESS_BAR);
	}

	/**
	 * Create a {@link NumberRangeView} as a slider
	 * @param value				The value to display, as a {@link Ref}
	 * @param min				The minimum value in range
	 * @param max				The maximum value in range
	 * @param locked			While this is true, view will not permit editing (control disabled) 
	 * @return					a new {@link NumberRangeView}
	 */
	public static NumberRangeView createSlider(final Ref<Integer> value, int min, int max, Ref<Boolean> locked) {
		return new NumberRangeView(value, min, max, locked, Views.updateManager(), ControlType.SLIDER);
	}

	/**
	 * Create a {@link NumberRangeView} as a progress bar
	 * @param value				The value to display, as a {@link Ref}
	 * @param min				The minimum value in range
	 * @param max				The maximum value in range
	 * @param locked			While this is true, view will not permit editing (control disabled) 
	 * @return					a new {@link NumberRangeView}
	 */
	public static NumberRangeView createProgressBar(final Ref<Integer> value, int min, int max, Ref<Boolean> locked) {
		return new NumberRangeView(value, min, max, locked, Views.updateManager(), ControlType.PROGRESS_BAR);
	}
	
	/**
	 * This is the same as the parent {@link JComponent}, 
	 * the only difference is that it remembers its {@link JView}, 
	 * so that as long as the {@link JComponent} is in
	 * use, the {@link JView} survives as well. Otherwise, the 
	 * {@link JView} can be GCed while the {@link JComponent} is still 
	 * in use, stopping the {@link JComponent} being updated.
	 * Otherwise, it is very easy for the {@link JView} to be collected,
	 * since a {@link View} is NOT kept alive by the data it is
	 * watching, and is generally not of much interest after its
	 * component has been added to a UI. 
	 */
	public static class LinkingJSlider extends JSlider {
		private final JView owner;
		private LinkingJSlider(JView owner, BoundedRangeModel brm) {
			super(brm);
			this.owner = owner;
		}
		/**
		 * Get the owner {@link JView}
		 * @return	The owner of this component
		 */
		public JView owner() {
			return owner;
		}
	}
	
	/**
	 * This is the same as the parent {@link JComponent}, 
	 * the only difference is that it remembers its {@link JView}, 
	 * so that as long as the {@link JComponent} is in
	 * use, the {@link JView} survives as well. Otherwise, the 
	 * {@link JView} can be GCed while the {@link JComponent} is still 
	 * in use, stopping the {@link JComponent} being updated.
	 * Otherwise, it is very easy for the {@link JView} to be collected,
	 * since a {@link View} is NOT kept alive by the data it is
	 * watching, and is generally not of much interest after its
	 * component has been added to a UI. 
	 */
	public static class LinkingJProgressBar extends JProgressBar {
		private final JView owner;
		private LinkingJProgressBar(JView owner, BoundedRangeModel brm) {
			super(brm);
			this.owner = owner;
		}
		/**
		 * Get the owner {@link JView}
		 * @return	The owner of this component
		 */
		public JView owner() {
			return owner;
		}
	}
}
